Skip to content

MSG-488 feat: [웹] AI 경로추천 페이지 — 자연어 요청·결과 카드·지도 오버레이 - #104

Merged
gyuhochoime merged 5 commits into
developfrom
feat/MSG-488-ai-route-page
Aug 28, 2026
Merged

MSG-488 feat: [웹] AI 경로추천 페이지 — 자연어 요청·결과 카드·지도 오버레이#104
gyuhochoime merged 5 commits into
developfrom
feat/MSG-488-ai-route-page

Conversation

@gyuhochoime

@gyuhochoime gyuhochoime commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

🎫 관련 티켓

📌 작업 내용

AI 경로추천 페이지(/ai-route)를 신설했습니다. 자연어 한 문장 → POST /api/routes/recommend → 좌측 388px 패널의 카드 리스트 + 지도 오버레이(번호 마커 · 격자 초록 틴트 · 지점 간 직선)까지가 이 티켓의 범위입니다.

웨이브 1 — MSG-489(출발지 자동 판정·mentionedArea 이동·1km 축척 고정·2차 자동 재요청·토스트)와 MSG-490(walk-paths 실경로·실거리)이 이 PR 머지 후 병렬로 얹힙니다.

주요 결정

결정 내용
라우트명 변경 티켓 가칭 /route/ai-route (NavKey aiRoute). 코드에 route가 이미 3중 의미로 존재 — ROUTES(react-router 상수) · ThemeId "route"(기존 코스 칩 "경로추천", 같은 theme-route 초록 공유) · map-overlay-store.routes
로그인 전용 2026-08-28 익명 POST 실측이 401 {developCode: 2403}RequireAuth + 레일 비로그인 클릭 시 login-modal-store 게이트
결과 도착 시 지도 이동·확대 없음 사용자가 보던 범위를 뺏지 않는 것이 이 티켓의 계약. 카드 클릭 시에만 moveTo(줌 불변). 자동 이동은 MSG-489 몫
번호 마커 Figma 정본화 28px(size-7) + border-2 border-background + shadow-raised. routeMarkerContent 공유상 기존 코스 칩 경유지 마커도 함께 바뀜 — 두 마커가 같은 초록이라 통일이 낫다는 판단으로 승인됨(markerStyle 분기 미생성)
결과 부족 배너 문구 FE 고정 서버 noticenull 여부 신호로만 사용하고 문자열은 화면에 노출하지 않음(테스트로 not.toContain 고정). 0곳이면 카드·오버레이 없이 배너만

재사용 성과 — 신규 렌더 코드가 거의 없습니다

map-overlay-store에 이미 routes(폴리라인 + 번호 경유지)·cells(색·빗금) 슬롯이 있어 MapShell → MapCanvas 렌더 경로를 통째로 재사용했습니다. MapCanvas 추가분은 경유지 onClick 슬롯 + active 강조뿐이고, onRouteWaypointClick을 주지 않으면 기존 코스 마커는 비클릭 그대로입니다. 오버레이 파생은 순수 함수(route-overlay.ts)에 두어 RN 경계를 지켰습니다(features/·model/naver import 0건).

ui-web 승격 2건

  • Skeleton 신설 — 레포 첫 스켈레톤입니다. 착수 전제였던 "도감 GalleryTabBody가 첫 사용처"는 실측 오류였습니다(GallerySkeletonDotsLoader 래퍼이고 MSG-403이 의도적으로 도트로 통일한 자리) → GalleryTabBody 미접촉
  • RetryNotice 이동pages/map-home/ui/packages/ui-web/. 재사용하면 pages → pages import가 되기 때문이며, 소비 8파일은 import 한 줄씩만 바뀝니다(렌더 불변)

웨이브 2 파일 소유권 — 교차 파일 0건

489∥490 병렬 머지에서 충돌이 나지 않도록 확장점을 갈라 두는 것이 이 티켓의 설계 산출물입니다. 전체 표는 docs/spec/MSG-488.md §4-2에 있습니다.

MSG-489 MSG-490
확장 ai-route-store · route-request · use-route-recommend · RouteInputCard (+ 신규 4파일) route-legs · use-route-legs · RouteWalkConnector · route-overlay (+ 신규 walk-paths 쿼리)
AiRoutePage.tsx 2줄 0줄
MapCanvas · map-overlay-store · 라우트 · 레일 · ui-web 0줄 (488 완결) 0줄 (488 완결)

확장점 주석 3종을 지정 위치에 심었고(RouteInputCard.tsx:45 · AiRoutePage.tsx:94 · use-route-legs.ts:11), 489·490의 신규 예정 파일은 하나도 만들지 않았습니다. features/map-home/model/map-scale.ts 미접촉.

apps/mobile · packages/ui-native diff 0줄입니다.

✅ 체크리스트

  • pnpm lint / pnpm typecheck / pnpm --filter web test run 통과 — 루트 게이트로 실행(ui-web·design-tokens 변경이 있어 패키지·모바일 파생 회귀를 잡기 위함, MSG-474·476 교훈)
  • 수용 기준 검증 완료 (아래 검증 요약)
  • 문서 업데이트 — docs/STATUS.md(라우트 표 · pages 4→5 · features 9→10 · widgets · ui-web 23→25 · 이력) + docs/spec/MSG-488.md 승격

🔍 검증 요약

검증자가 빌더 수치를 믿지 않고 루트에서 직접 재실행한 결과입니다.

게이트 명령 실측
테스트 pnpm test web 193 files / 1405 tests + mobile 156 / 982 · exit 0
타입 pnpm typecheck 6/6 워크스페이스 Done · exit 0
린트 pnpm lint oxlint exit 0 · 신규 eslint-disable/@ts-ignore 0건
포맷 pnpm format:check 1109 files OK
중복 pnpm check:duplication 0 families · exit 0
생성물 드리프트 pnpm openapi-tsgit diff --exit-code diff 없음 — SDK 재생성 흔적 없음
빌드 pnpm build exit 0

test-first: 구현 착수 전 RED 1회 실측(11 failed / 180 passed 파일). 기존 테스트 약화·skip·삭제 0건.

기준별 판정

로직 L1~L13 — 전부 통과. 검증자가 빌더 매핑 표를 베끼지 않고 테스트 본문과 구현을 대조했습니다.

  • L5(에러 7행)는 toEqual 완전 객체 비교로 문구를 문자 단위 고정
  • L7(잔상 0)은 구현이 단일 원자 set이라 "로딩 전에 먼저 비워진다"가 구조적으로 보장됨
  • L4는 서버 notice 미노출을 not.toContain으로 직접 증명
  • L11은 Object.keys(ROUTES) 순서까지 고정

화면 S1~S14 — 전부 통과 (S6의 빗금 1항목만 확인불가).

브라우저에 남아 있던 리프레시 쿠키로 앱이 실제 JWT를 자동 재발급해 실계정 세션으로 검증했고, 첫 제출은 실 API로 8곳을 받았습니다(자격증명 입력 없음). 상태별 재현(부족·0곳·실패 7종)은 window.fetch 스텁 구동.

핵심 3건 실측:

  • S6 지도 미이동 — 제출 전/후 지도 중심·줌 완전 동일(축척바 100m 불변)
  • S7 잔상 0 — 8곳 → 3곳 → 2곳 → 0곳 4회 연속 갱신하며 매번 이전 마커·격자·폴리라인 전부 소거
  • S11 섹션 복귀 — 도감에서 오버레이 완전히 걷히고, 복귀 시 입력·카드·지도 전부 복원 + 재요청 0회

그 외: S8 카드↔마커 양방향(카드 클릭 시 줌 불변 이동 / 마커 클릭 시 지도 미이동), S10 실패 7경로 전건, S12 레일 재클릭 2단, 콘솔 에러 0건. 비로그인 직접 진입 → 홈 리다이렉트 + 모달도 실측.

Figma 대조 — 4프레임(15666:12402 · 12621 · 12855 · 13416) 일치, 스펙 §8 "오탐 방지" 목록 밖 편차 0건.

규칙 감사 — FSD·재사용·RN 경계·토큰 위반 0건. features/·model/에 지도 SDK import 0건, pages → pages import 없음.

확인불가 2건

  • S6 빗금(추천 격자 ∩ 내 점령 격자) — 실데이터에 교집합이 발생하지 않아 육안 불가. L6 단위 테스트(hatched: [false, true, false])로 대체 확인
  • 키보드 순회 종단 재현 — 자동화 도구의 합성 키 이벤트가 페이지에 전달되지 않음. 구조 증거로 대체: 패널 3개 상태 전부 비네이티브 인터랙티브 요소 0개(div role="button"·임의 tabindex 없이 전부 실제 <button>·<textarea>)라 Tab 도달·Enter/Space 활성화가 플랫폼 기본으로 보장되며, 각 요소 focus() 도달을 전수 확인

검증이 잡아 수정한 것

a11y 낭독 중복RouteResultHeader(role="status")가 자체 role="status"를 가진 ui-web DotsLoader를 감싸 "AI 추천 · 동선 찾는 중 동선 찾는 중 지금 지도 범위 기준"으로 두 번 읽혔습니다. 도트를 aria-hidden으로 감싸 해소했고(ui-web 무수정 — 다른 소비처 파급 회피), 재검증에서 노출된 중첩 라이브 리전 0개 · 헤더 한 줄 레이아웃 동일 · 시각 회귀 없음을 확인했습니다.

codex 리뷰 (push 전 게이트) — 지적 1건 반영

[P2] 지도 준비 전에도 제출 버튼이 활성이었다. 지도 초기화가 느리거나 실패해 viewport-store.boundsnull인 동안에도 문장만 유효하면 버튼이 활성이었고, 클릭하면 buildRecommendBodynull을 내며 submit이 조용히 early-return했습니다 — "눌러도 아무 일도 안 일어나는 버튼". 원인은 canSubmit(L8)과 buildRecommendBody(L9)의 성립 조건이 갈라져 있었던 것입니다.

canSubmitmapReady(= bounds !== null)를 추가해 두 함수의 판정 집합을 일치시켰고, 테스트 2건을 더했습니다(RED 실측 후 GREEN) — 하나는 mapReady: false 차단, 다른 하나는 canSubmit === truebuildRecommendBody가 반드시 요청을 만든다는 정합을 양방향으로 고정해 두 조건이 다시 갈라지는 것을 막습니다.

검증 리포트가 "미세 갭 ③"으로 남겨 둔 것과 같은 자리입니다 — 단위 테스트가 두 함수를 각각 검사했지만 둘 사이의 계약은 아무도 검사하지 않았습니다. 반영 후 루트 게이트 전건 그린(web 193 files / 1407 tests, +2).

💡 추가 논의할 사항

  1. MSG-489 선행 확인 — "지도 위 현재 위치 점"을 그릴 슬롯이 MapCanvasProps에 없습니다(MapLabelOverlay는 텍스트 pill, waypoints는 번호 뱃지). §4-2가 "488 완결 / 489 0줄"로 선언한 MapCanvas.tsx를 489가 다시 열 공산이 큽니다. 489∥490 병렬성 자체는 안 깨집니다(490은 MapCanvas 0줄) — 489 스펙 단계에서 범용 점 슬롯 추가를 계획에 넣으면 됩니다.
  2. 알려진 한계 (WAIVED)MapShell.gateFillCellszoom < 16에서 채움 셀을 버려, 넓게 본 상태에서 요청하면 격자 초록 틴트가 안 보입니다(마커·선은 남음). 488은 지도를 이동·확대하지 않으므로 재현되며, MSG-489의 1km 축척 고정이 해소합니다.
  3. 선택 마커 강조는 근사 — 스펙이 "선택 시 확대·테두리 강조"만 지시했고 Figma에 선택 변형 노드가 없어 scale-125 ring-2 ring-theme-route로 구현했습니다. 디자인 재확인 대상.
  4. nose baseline 재등재 — 착수 전 클린 트리(107aa59)에서 이미 exit 1 / 3 families였고 근원은 이 티켓 자신의 웨이브 0 openapi 커밋입니다. 검증자가 git archive HEAD로 워크트리 밖에 풀어 독립 재현했고, 추가 30건 중 "멤버 전원 변경 집합 밖" 9건도 id 단위로 일치 — 숨어든 진짜 중복 0건.
  5. 비로그인 정책은 스냅샷 — 401(2403)은 2026-08-28 실측입니다. 서버가 익명 허용으로 바뀌면 게이트를 되돌려야 합니다(MSG-474가 정확히 그런 뒤집기였습니다).

gyuhochoime and others added 3 commits August 28, 2026 15:00
codex 리뷰 P2. viewport bounds가 null인 동안(지도 초기화 지연·실패) 문장만
유효하면 버튼이 활성이었고, 클릭하면 buildRecommendBody가 null을 내며 submit이
조용히 early-return해 "눌러도 안 되는 버튼"이 됐다.

canSubmit에 mapReady(= bounds !== null)를 추가해 L8·L9 판정 집합을 일치시키고,
두 함수의 정합을 양방향으로 고정하는 테스트를 더했다 — 단위 테스트가 각 함수는
검사했지만 둘 사이의 계약은 아무도 검사하지 않던 자리다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ki5yWGDS5mmVW3djBBqvkX
@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown

Claude finished @gyuhochoime's task in 6m 7s —— View job


✅ 코드 리뷰 완료

작업 목록

  • 컨텍스트 파악 (PR 본문, diff, CLAUDE.md)
  • 신규 파일 리뷰 — features/ai-route/* (model/api)
  • 신규 파일 리뷰 — pages/ai-route/* (UI 컴포넌트)
  • 기존 파일 변경 리뷰 — widgets/map-shell, widgets/side-rail-nav, app/router.tsx
  • 기존 파일 변경 리뷰 — packages/ui-web 신규 컴포넌트 (Skeleton, RetryNotice)
  • 인라인 코멘트 작성 (2건)
  • 전체 요약 코멘트 게시

요약: 전반적으로 매우 꼼꼼한 구현입니다 (로직 순수 함수 + 유닛 테스트, RN 경계 준수, canSubmitbuildRecommendBody 일치 등). 인라인으로 🟡 1건(결과 화면에서 타이핑 시 지도 오버레이 store가 불필요하게 재게시될 수 있는 렌더 효율성 이슈, 추정)과 🟢 1건(footnote 문단 중복)을 남겼습니다. 전체 요약은 별도 코멘트로 게시했습니다: #104 (comment)

@github-actions

github-actions Bot commented Aug 28, 2026

Copy link
Copy Markdown

React Doctor found 1 new issue in 1 file · 1 warning · score 92 / 100 (Great) · 0 fixed · vs develop

1 warning

src/features/ai-route/api/use-route-recommend.ts

  • ⚠️ L30 Mutation without cache invalidation query-mutation-missing-invalidation

Reviewed by React Doctor for commit 2445fc7. See inline comments for fixes.

Comment thread apps/web/src/pages/ai-route/ui/use-ai-route-overlay-publish.ts
Comment thread apps/web/src/pages/ai-route/AiRoutePage.tsx
@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown

🔍 코드 리뷰 요약 — MSG-488 AI 경로추천 페이지

전반 평가: 매우 꼼꼼합니다. features/ai-route/model/*가 전부 순수 함수 + 유닛 테스트로 격리돼 있고, pages/ai-route/*는 그 파생 결과만 렌더하는 얇은 뷰 계층으로 잘 나뉘어 있습니다. RN 경계(로직에 naver import 0건), 기존 map-overlay-store/MapCanvas 재사용, canSubmitbuildRecommendBody 조건 일치(codex 리뷰 반영분) 등 설계 결정들이 코드에 그대로 반영돼 있는 걸 확인했습니다. route-error.ts의 developCode 매핑, ai-route-store.ts의 "새 요청은 이전 결과를 먼저 비운다" 원자적 set, 로그인 필요(2403) 시 에러 화면 없이 입력 대기로 되돌리는 분기 모두 설명한 대로 동작합니다.

🟡 권장 사항

  • apps/web/src/pages/ai-route/ui/use-ai-route-overlay-publish.ts:34-51 — (추정) AiRoutePageoccupiedGridIdsuseOccupiedGridsQuery의 미메모이즈된 flattenGridPages 호출 때문에 매 렌더 새 배열 참조가 되고, 이게 이 파일의 overlay/useEffect를 매 렌더 재실행시킵니다. 결과 카드가 떠 있는 상태에서 사용자가 textarea에 계속 타이핑(“다시 짜기” 준비)하면 그때마다 전역 map-overlay-store가 clear→재설정되어 지도 마커·폴리라인이 불필요하게 재생성될 수 있습니다. 같은 패턴이 MapShell.tsx:149-151에도 이미 있어 이 PR이 새로 만든 문제는 아니지만, 여기서는 전역 store effect로 이어져 체감 영향이 다를 수 있습니다. 상세 제안은 인라인 코멘트 참고.

🟢 사소한 제안

  • apps/web/src/pages/ai-route/AiRoutePage.tsx:107-130RESULT_FOOTNOTE 문단이 로딩/결과 블록에 그대로 중복돼 있습니다. loading || status === "result" 조건으로 한 번만 렌더하도록 합치면 중복을 줄일 수 있습니다.
  • (참고, 추측) apps/mobile의 두 테스트 픽스처 파일(profile-image.parity.test.ts, reset-session-cache.test.ts)에 role: "USER" 필드가 1줄씩 추가돼 있습니다. PR 본문의 "apps/mobile diff 0줄" 문구는 앱 소스 기준이고 이건 웨이브 0 OpenAPI 커밋(UserProfile.role 필수화)에 따른 테스트 픽스처 보정으로 보여 문제는 아닙니다.

확인한 항목 (문제 없음)

  • route-request.tscanSubmit/buildRecommendBody 조건 일치(P2 수정) — 두 함수 모두 mapReady(bounds !== null)를 요구해 "눌러도 안 되는 버튼" 케이스가 재현되지 않습니다.
  • route-error.tsApiError 아님/네트워크 에러(status undefined)/developCode 미매핑/401 폴백 분기 모두 커버.
  • RouteErrorNotice.tsx — 14503(기능 꺼짐)은 재시도 버튼 없이 문구만, canSubmitfeatureDisabled가 제출을 계속 막음.
  • route-overlay.ts — gridId 중복 제거, 점령 격자 교집합 시 빗금, 지점 0개면 오버레이 전부 빈 배열(잔상 없음).
  • SideRailNav.tsx/rail-action.ts — 비로그인 클릭 시 로그인 모달, 재클릭 2단(리셋→토글) 배선 정확.
  • RequireAuth로 직접 URL 진입 방어 — 기존 컴포넌트를 그대로 재사용.
  • packages/ui-webSkeleton/RetryNotice 승격 — 도메인 무관, 기존 소비처(8곳) import 한 줄 교체만 확인.
  • XSS 관점 — routeMarkerContent에 들어가는 seq는 타입상 number(order/waypoint 인덱스)라 서버 문자열이 직접 HTML로 꽂히지 않습니다.

큰 로직 결함은 발견하지 못했습니다. 위 🟡 항목은 기능 정확성보다는 렌더 효율성 쪽 이슈입니다.

useOccupiedGridsQuery가 flattenGridPages 결과를 memo 없이 반환해 데이터가 그대로여도
매 렌더 새 배열이 나왔다. AiRoutePage는 그 파생을 전역 map-overlay-store에 effect로
게시하므로, 결과 화면에서 타이핑할 때마다 clear → 재게시가 돌아 naver Marker·Polyline이
재생성됐다. renderHook 연속 rerender로 RED 재현 후 근원에서 useMemo로 고정했다.

모바일 이식본(apps/mobile/.../use-occupied-grids-query.ts:69)은 이미 같은 가드를
갖고 있었다 — 웹 원본만 빠져 있었고, 이 수정은 패리티를 되돌린다. MapShell:149의
같은 패턴도 함께 해소된다.

RESULT_FOOTNOTE 문단은 로딩·결과 블록에 중복돼 있던 것을 한 곳으로 합쳤다.

nose 승인 패밀리 3건 시프트 재키잉(437→437, 2건은 digest 바이트 동일).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ki5yWGDS5mmVW3djBBqvkX
@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown

Claude finished @gyuhochoime's task in 3m 16s —— View job


✅ 코드 리뷰 완료

작업 목록

  • 컨텍스트 파악 (PR 본문, 최신 커밋 98d156d diff, CLAUDE.md)
  • 최신 커밋(98d156d)이 이전 리뷰 지적 2건을 실제로 반영했는지 확인
  • 신규/변경 파일 전수 재검토 (버그·React 베스트프랙티스·성능·가독성·보안)
  • 인라인 코멘트 작성 — 신규 이슈 없어 생략
  • 전체 요약 코멘트 게시 (gh pr comment)

요약: 최신 푸시(98d156d)는 이전 리뷰가 지적한 2건(occupiedGridIds 참조 불안정, RESULT_FOOTNOTE 중복)을 정확히 근원에서 반영했습니다. RouteInputCard·RouteStopCard·RouteResultList·route-request·route-error·SideRailNav/rail-action·MapCanvas/map-overlay-store diff 등 이전에 상세히 보지 못한 파일까지 직접 Read로 확인했고, 새로 발견된 버그·성능·보안 이슈는 없습니다. 전체 요약은 별도 코멘트로 게시했습니다: #104 (comment)

@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown

🔍 코드 리뷰 요약 — MSG-488 AI 경로추천 페이지 (최신 커밋 98d156d 기준)

컨텍스트: 이전 리뷰(703827d 기준)에서 지적된 2건 — ① occupiedGridIds 참조 불안정으로 인한 오버레이 effect 불필요 재실행, ② RESULT_FOOTNOTE 중복 렌더 — 을 반영한 후속 커밋(98d156d)에 대한 재검토입니다. 이번에는 이전에 상세히 보지 못했던 파일들(RouteInputCard·RouteStopCard·RouteResultList·RouteResultHeader·RouteErrorNotice·route-point-view·route-request·route-error·SideRailNav/rail-action·MapCanvas/map-overlay-store/MapShell diff·router.tsx)까지 Read로 직접 확인했습니다.

✅ 이전 지적사항 반영 확인

  • occupiedGridIds 참조 안정화use-occupied-grids-query.ts에서 flattenGridPages 결과를 useMemo(() => ..., [query.data?.pages])로 감쌌습니다. renderHook + 연속 rerender()로 RED 재현 후 GREEN 전환한 신규 테스트(use-occupied-grids-query.render.test.tsx)도 확인했습니다. 근원 수정이라 MapShell.tsx:149의 동일 패턴도 함께 해소됩니다. 모바일 이식본이 이미 같은 가드를 갖고 있었다는 근거도 코드상 타당합니다.
  • RESULT_FOOTNOTE 중복 제거AiRoutePage.tsx:126-131에서 {(loading || status === "result") && <p>...}로 한 곳에 합쳐졌고, 로딩/결과 블록 내부의 중복 렌더는 제거됐습니다.
  • React Doctor 자동 지적 2건(캐시 무효화 누락·toSorted) 기각 판단도 타당recommend는 서버 상태를 바꾸지 않는 조회성 POST라 무효화 대상 캐시가 없고, toSorted().oxlintrc.jsonapps/mobile/packages/ui-native에서 error로 금지하는 API(MSG-427 Hermes 크래시 선례)라 모바일 이식 대상 순수 함수에 지금 도입하면 재발 위험이 있습니다. 근거가 docs/spec/MSG-488.md에 기록돼 있습니다.

🟢 참고 (문제는 아님)

  • RouteResultList.tsx:35legs.find((item) => item.toOrder === point.order)가 카드 map 루프 안에서 매번 배열을 선형 탐색해 이론상 O(n²)이지만, AI 추천 지점 수가 소수(수 개~십여 개)라 실질적 영향은 없습니다.
  • RequireAuth(app/RequireAuth.tsx)를 통한 /ai-route 직접 URL 진입 방어가 기존 /dex·/profile과 동일한 "마운트 시점 스냅샷 → 홈 리다이렉트 + 로그인 모달" 패턴을 그대로 재사용하고 있어 일관적입니다.

종합 의견

새로 도입된 로직(route-request.tscanSubmitbuildRecommendBody 조건 일치, route-error.ts의 developCode 매핑, ai-route-store.ts의 원자적 상태 전이, route-overlay.ts/route-legs.ts의 순수 파생)과 뷰 계층(RouteInputCard·RouteStopCard·RouteResultHeader의 a11y 처리 등)을 직접 읽고 대조한 결과, 버그·로직 오류·불필요 리렌더·XSS·타입 안정성 관점에서 새로 발견된 문제는 없습니다. 이전 두 차례 리뷰(React Doctor 자동 분석 + Claude 코드 리뷰)에서 지적된 사항은 모두 적절히 반영되었거나 타당한 근거로 기각되었습니다. 별도 인라인 코멘트는 남기지 않았습니다(신규 이슈 없음).

View job run · 브랜치

PR #104 리뷰에서 react-doctor가 [...arr].sort()를 toSorted()로 바꾸라고 지적했는데,
.oxlintrc.json:84는 같은 API를 error로 금지하고 있다(Hermes 미구현 — MSG-427에서 지도
홈을 마운트 즉시 크래시). oxlint 금지가 mobile/ui-native 한정이라 웹 파일에서는
react-doctor만 말하고 게이트가 아무것도 안 잡는다. 웹 toSorted 6곳 중 모바일 이식본이
있는 5곳 전부가 손으로 되돌려져 Hermes 주석을 달고 있는 것이 그 비용의 실측이다.

단 이 설정은 현재 무효다: v0.9.3에서 rules list/explain은 off로 보고하고 공식
`react-doctor rules disable`이 쓴 것도 같은 내용인데 스캐너는 규칙을 그대로 보고한다
(루트·apps/web cwd 양쪽, ignore.tags도 동일). 기존 js-combine-iterations: off도 같은
이유로 무효였다. 실효 방어선은 CLAUDE.md 이력 기록이고, 설정은 도구가 고쳐지면 듣도록
선언만 남긴다.

CLAUDE.md 5행 유지 규칙에 따라 2026-08-19 e2e apt 행을 HARNESS_CHANGELOG로 이관.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ki5yWGDS5mmVW3djBBqvkX
@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown

Claude finished @gyuhochoime's task in 2m 15s —— View job


🔍 코드 리뷰 완료 (커밋 2445fc7 기준)

작업 목록

  • 컨텍스트 파악 — 이전 리뷰 2회(703827d, 98d156d 기준) 및 CLAUDE.md 확인
  • 이번 푸시 증분 diff 확인 (98d156d..2445fc7)
  • 변경 파일 상세 검토 (doctor.config.json, CLAUDE.md, docs/HARNESS_CHANGELOG.md)
  • 전체 요약 코멘트 게시

요약: 이번 푸시는 코드 변경 없이 doctor.config.json 규칙 off 선언 + 하네스 이력 문서(CLAUDE.md/docs/HARNESS_CHANGELOG.md) 갱신뿐입니다. React Doctor가 지적한 toSorted 권고를 왜 기각하는지(oxlint의 Hermes 금지 규칙과 상충, 근거 자체 오류) 근거와 함께 잘 기록했습니다. 새로 발견된 버그나 이슈는 없습니다. 별도 코멘트로 상세 리뷰를 게시했습니다.
· 브랜치

const succeed = useAiRouteStore((s) => s.succeed);
const fail = useAiRouteStore((s) => s.fail);

return useMutation({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React Doctor · react-doctor/query-mutation-missing-invalidation (warning)

useMutation with no cache update here can leave your users looking at stale data after it runs.

Fix → Add onSuccess: () => queryClient.invalidateQueries({ queryKey: ['...'] }) so cached data stays in sync after the mutation

Docs

@gyuhochoime

Copy link
Copy Markdown
Contributor Author

React Doctor 지적 3건 — 전부 기각 (근거 첨부)

js-tosorted-immutable ×2, query-mutation-missing-invalidation ×1을 검토했고 코드는 고치지 않습니다. 사유를 남깁니다.

1. toSorted() 권고 (route-legs.ts · route-overlay.ts) — 기각

이 레포에서 두 도구가 정반대를 지시하고 있습니다.

  • react-doctor: "[...arr].sort()toSorted()"
  • .oxlintrc.json:84 (error): "toSorted 금지 — Hermes 미구현. [...arr].sort(...)를 쓸 것"

oxlint 금지가 apps/mobile·packages/ui-native 한정이라 웹 파일에서는 react-doctor만 말하고 게이트 6종이 아무것도 잡지 못합니다 — vitest는 Node에서 돌아 메서드가 존재하고, typecheck의 lib에는 ES2023이 있습니다. MSG-427에서 이 API가 지도 홈을 마운트 즉시 크래시시켰을 때 게이트가 전부 통과한 이유입니다.

비용은 이미 측정돼 있습니다. 웹 프로덕션 toSorted 6곳 중 모바일 이식본이 있는 5곳 전부가 손으로 [...].sort()로 되돌려져 Hermes 주석을 달고 있습니다:

모바일 이식본
hot-region-summary.ts:19 :21 — "Hermes(RN 0.86)에 toSorted 미구현"
course.ts:66 :63
use-multi-grid-videos-query.ts:54 :55
region-cluster-overlay.ts:200 :115
gallery-groups.ts:34 :37

뒤 두 개는 CLAUDE.md가 MSG-427 사고로 지목한 바로 그 파일들입니다 — "같은 함정이 두 번 주석으로 문서화돼 있었는데도 재발했다."

이번 지적 대상인 route-legs.ts:6은 파일 스스로 **(RN 재사용 대상)**이라 선언하고 있고, 모바일에는 이미 features/map-home/model/route-overlay.ts 이식본이 존재합니다. 권고를 따르면 6번째 이식 세금이 확정됩니다.

덧붙여 규칙의 근거 자체가 사실과 다릅니다. "복사 없이 정렬한다"고 하지만 toSorted()도 새 배열을 할당합니다. 성능 이득은 없고 순수 스타일 변경입니다.

2. invalidateQueries 권고 (use-route-recommend.ts) — 기각

recommend서버 상태를 바꾸지 않습니다. 요청 본문이 필요해 POST일 뿐 조회입니다. 결과 정본은 useAiRouteStore(zustand)이고 이 데이터를 담는 쿼리 캐시가 없어 무효화할 대상이 존재하지 않습니다.

오히려 무효화를 넣으면 수용 기준 **S11(섹션 왕복 시 재요청 0회)**이 깨집니다 — 도감에 갔다 돌아왔을 때 결과가 그대로 복원되는 것이 이 화면의 계약입니다.

후속 — 설정으로 못 막습니다 (도구 버그 실측)

같은 경고가 앞으로도 계속 뜰 것이라 doctor.config.json에 규칙 off를 선언했는데, v0.9.3 스캐너가 rules 설정을 무시합니다. rules list·rules explainoff로 보고하고 공식 react-doctor rules disable이 쓴 내용도 동일한데 스캔 결과는 그대로입니다(레포 루트·apps/web cwd 양쪽에서 재현, ignore.tags도 동일하게 무시). 기존 js-combine-iterations: "off" 항목도 같은 이유로 무효였습니다.

선언은 도구가 고쳐지면 자동으로 듣도록 남겨 두고, 실효 방어선은 CLAUDE.md 변경 이력에 근거와 함께 기록했습니다(커밋 2445fc7).

별도 티켓 후보: oxlint의 toSorted 금지를 레포 전역으로 승격하면 이식 세금이 원천 제거됩니다. 다만 기존 웹 6곳 수정이 따라오므로 이 PR 범위 밖입니다.

@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown

🔍 코드 리뷰 요약 — MSG-488 AI 경로추천 페이지 (최신 커밋 2445fc7 기준)

컨텍스트: 이전 두 차례 리뷰(703827d·98d156d 기준)에서 pages/ai-route/*·features/ai-route/*·widgets/map-shell·widgets/side-rail-nav·packages/ui-web 등 실제 코드는 이미 전수 검토했고, 지적된 2건(occupiedGridIds 참조 불안정, RESULT_FOOTNOTE 중복)도 모두 반영·확인됐습니다. 이번 푸시(2445fc7)는 코드 변경이 전혀 없고, React Doctor 자동 리뷰가 남긴 js-tosorted-immutable 경고 2건(route-legs.ts:37, route-overlay.ts:44)에 대한 대응을 doctor.config.json 설정과 하네스 이력 문서로 남긴 chore 커밋입니다.

변경 파일 검토

  • doctor.config.jsonreact-doctor/js-tosorted-immutableoff로 추가. JSON 문법 확인, 기존 js-combine-iterations: off와 같은 배열 구조로 일관성 있음.
  • CLAUDE.md — "변경 이력" 표에 2026-08-28 행 추가(최상단), 5행 유지 규칙에 따라 가장 오래된 2026-08-19(e2e apt 미러) 행을 docs/HARNESS_CHANGELOG.md로 이관. 표 포맷·컬럼 정합성 이상 없음.
  • docs/HARNESS_CHANGELOG.md — 이관된 행이 정확히 동일한 텍스트로 말미에 추가됨. 중복·누락 없음.

결정 타당성 (참고 의견)

기록된 근거를 코드로 직접 대조해봤습니다.

  • .oxlintrc.jsonapps/mobile·packages/ui-native에서 toSorted 등을 error로 금지하는 것(MSG-427 Hermes 크래시 선례)과, React Doctor가 반대로 toSorted() 사용을 권고하는 것이 실제로 상충합니다 — 웹 전용 파일(route-legs.ts/route-overlay.ts)은 RN 이식 대상으로 문서화돼 있어(route-legs.ts:6 등) 권고를 따르지 않는 판단이 합리적입니다.
  • "설정이 v0.9.3 스캐너에 안 먹는다"는 실측 기록은 코드로 재현 검증할 수 있는 대상은 아니라(외부 도구 동작) 기록된 내용을 그대로 신뢰했습니다 — 이 부분은 검증하지 못했고 기록을 신뢰한 것임을 명시합니다.
  • 설정이 무효라도 "도구가 고쳐지면 자동으로 듣게" 선언만 남긴다는 접근은 합리적이며, 실효 방어선(문서 기록)과 잠재적 방어선(설정)을 분리한 점이 좋습니다.

참고 (이 커밋과 무관, PR 범위 밖 관찰)

체크아웃된 워크트리에서 CLAUDE.md커밋되지 않은 로컬 수정이 남아 있어(git diff HEAD -- CLAUDE.md), 이번 커밋이 추가한 2026-08-28 행과 이관한 2026-08-19 행이 워킹 트리 상에서는 되돌려져 있는 상태를 발견했습니다. PR의 커밋 이력 자체는 정상이고 이 리뷰가 대상으로 삼은 것도 커밋된 diff이므로 리뷰 결론에는 영향이 없지만, CI 체크아웃 환경에 원인 불명의 이형(異形)이 있다는 점은 참고로 남깁니다(제가 만든 변경은 아닙니다 — 세션 시작 시점 git status에 이미 M CLAUDE.md로 잡혀 있었습니다).

종합 의견

이번 푸시로 인한 신규 버그·로직 오류·성능·보안 이슈는 없습니다(변경분이 코드가 아니므로 해당 리뷰 관점 대부분 적용 대상 아님). 이전 리뷰에서 확인한 코드 품질 판단도 그대로 유효합니다. 머지 관점에서 추가로 막을 이슈는 발견하지 못했습니다.

View job run · 브랜치

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant